<template>
  <a-form :model="form">
    <a-form-item
      label="accessKey"
      name="ak"
      :rules="[{ required: true, message: 'Please input your accessKey!' }]"
    >
      <a-input v-model:value="form.ak" />
    </a-form-item>
    <a-form-item
      label="secretKey"
      name="sk"
      :rules="[{ required: true, message: 'Please input your secretKey!' }]"
    >
      <a-input v-model:value="form.sk" />
    </a-form-item>
    <a-form-item
      label="bucket"
      name="bucket"
      :rules="[{ required: true, message: 'Please input your bucket!' }]"
    >
      <a-input v-model:value="form.bucket" />
    </a-form-item>
    <a-form-item label="parentPath" name="parentPath">
      <a-input v-model:value="form.parentPath" />
    </a-form-item>
  </a-form>
  <div id="drag-drop-area" ref="dragDropRef">将文件夹拖拽至此处上传</div>
  <div class="statistic-box">
    <a-statistic title="待上传" :value="uploadQueue.length" />
    <a-statistic title="上传中" :value="uploadingQueue.length" />
    <a-statistic title="上传成功" :value="successQueue.length" />
    <a-statistic title="上传失败" :value="failQueue.length" />
  </div>
  <div class="error-box">
    <a-list item-layout="horizontal" :data-source="failQueue">
      <template #renderItem="{ item }">
        <a-list-item>
          <a-list-item-meta :description="item.error.message">
            <template #title
              >{{ item.parentPath || item.entry.name }}
            </template>
          </a-list-item-meta>
        </a-list-item>
      </template>
    </a-list>
  </div>
</template>

<script setup>
import { reactive, ref, getCurrentInstance, onMounted, watch } from "vue";
import * as qiniu from "qiniu-js";
import { message } from "ant-design-vue";

const { proxy } = getCurrentInstance();

const dragDropRef = ref();
onMounted(() => {
  dragDropRef.value.addEventListener("dragover", function (e) {
    e.preventDefault();
  });

  dragDropRef.value.addEventListener("drop", function (e) {
    e.preventDefault();
    upload(e.dataTransfer.items, form.value.parentPath);
  });
});

// 获取token
const form = ref({ ak: "", sk: "", bucket: "", parentPath: "" }),
  getToken = (key = "") => {
    return new Promise((resolve, reject) => {
      fetch("http://api.course.huikaoedu.com/course/qiniu/getUploadToken", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ key, ...form.value }),
      }).then((res) => {
        if (res.ok) {
          res.json().then((json) => {
            if (json.code == 200) resolve(json.data);
            else {
              resolve();
              message.error(json.msg);
            }
          });
        }
      });
    });
  };

// 最大上传线程数
const MAX_UPLOAD_THREADS = 5,
  // 待上传队列
  uploadQueue = reactive([]),
  // 上传中队列
  uploadingQueue = reactive([]),
  // 上传成功队列
  successQueue = reactive([]),
  // 上传失败队列
  failQueue = reactive([]);

// 上传：获取文件列表，并上传文件列表
const upload = (items, parentPath) => {
  // uploadingQueue.length = 0;
  // successQueue.length = 0;
  // failQueue.length = 0;
  // 获取文件列表
  getItems(items, parentPath).then((res) => {
    // 上传文件列表
    return uploadFiles();
  });
};

// 获取内容项列表
const getItems = (items, parentPath) => {
  const promises = [];

  for (const item of items) {
    const entry = item.webkitGetAsEntry();
    if (entry.isFile) promises.push(getFile(entry, parentPath));
    else if (entry.isDirectory) promises.push(getDirectory(entry, parentPath));
  }

  return Promise.all(promises);
};

// 获取文件
const getFile = (fileEntry, parentPath) => {
  uploadQueue.push({ entry: fileEntry, parentPath: parentPath });
};

const readEntries = (reader) => {
  return new Promise((resolve, reject) => {
    reader.readEntries(resolve, reject);
  });
};

// 获取文件夹
const getDirectory = async (directoryEntry, parentPath) => {
  const fullPath = parentPath
    ? `${parentPath}/${directoryEntry.name}`
    : directoryEntry.name;

  let childEntries = [];
  const reader = directoryEntry.createReader();

  while (childEntries.length >= 0) {
    childEntries = await readEntries(reader);

    if (childEntries.length == 0) break; // 退出循环，避免无限循环
    for (const entry of childEntries)
      if (entry.isFile) getFile(entry, fullPath);
      else if (entry.isDirectory) getDirectory(entry, fullPath);
  }
};

// 上传单个文件
const uploadFile = async (fileEntry, parentPath) => {
  return new Promise(async (resolve, reject) => {
    fileEntry.file(async (file) => {
      const fullPath = parentPath ? `${parentPath}/${file.name}` : file.name;

      getToken(fullPath).then((token) => {
        if (token) {
          const observable = qiniu.upload(file, fullPath, token);
          observable.subscribe(
            () => {},
            (err) => {
              resolve({ success: false, data: err });
            },
            (res) => {
              resolve({ success: true, data: res });
            }
          );
        } else {
          resolve({
            success: false,
            data: { message: "token生成失败" },
            parentPath,
            fileEntry,
          });
        }
      });
    });
  });
};

// 上传文件列表
const uploadFiles = () => {
  while (uploadQueue.length > 0 && uploadingQueue.length < MAX_UPLOAD_THREADS) {
    const uploadEntry = uploadQueue.shift(),
      entry = uploadEntry.entry,
      parentPath = uploadEntry.parentPath;

    uploadingQueue.push(uploadEntry);

    uploadFile(entry, parentPath)
      .then((res) => {
        if (res.success) {
          // 上传成功
          successQueue.push({ result: res.data, entry, parentPath });
        } else {
          // 上传失败
          console.log("Error: " + res.data.message);
          failQueue.push({ error: res.data, entry, parentPath });
        }
      })
      .finally(() => {
        const index = uploadingQueue.indexOf(entry);
        uploadingQueue.splice(index, 1);
        uploadFiles();
      });
  }
  if (uploadQueue.length <= 0 && uploadingQueue.length <= 0) {
    message.info("上传完成！");
    // 所有文件上传完成
    console.log(
      "All files uploaded！success:",
      successQueue.length,
      " fail:",
      failQueue.length
    );
  }
};
</script>

<style lang="less" scoped>
#drag-drop-area {
  width: 600px;
  height: 400px;
  border: 1px dashed;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: auto;
}

.statistic-box {
  width: 600px;
  display: flex;
  justify-content: space-around;
  margin: 15px auto 0 auto;
}

.error-box {
  width: 600px;
  margin: auto;
}

form.ant-form.ant-form-horizontal {
  width: 600px;
  margin: 15px auto 0 auto;
}
</style>
